#!/usr/bin/python2.6
"""
clogger.py MODE [ --param --param ... ] FILE

Where MODE is one of the listed modes for processing the specified file.


"""

# TODO: add help

import datetime
import optparse
import re
import sys
import os
import stat
import time
import dateutil.parser as dateutil_parser

from clogger_flags import *
import clogger_sinks

def ParseTime(s):
  ago_re = re.compile(r'^(\d+) (days?|hours?|minutes?|seconds?) ago$')
  time_multiplier = dict(days=86400, hours=3600, minutes=60, seconds=1,
                         day=86400, hour=3600, minute=60, second=1)
  m = ago_re.match(s)
  if m:
    now = datetime.datetime.now()
    delta_seconds = int(m.group(1)) * time_multiplier[m.group(2)]
    delta = datetime.timedelta(0, delta_seconds)
    return now - delta
  parser = dateutil_parser.parser()
  return parser.parse(s)

class LogReader(object):
  def __init__(self, filename, start_time):
    self._fh = open(filename, "rb")
    if not start_time: return

    low, high = -1, os.stat(filename)[stat.ST_SIZE]
    while high - low > 1:
      probe = (low + high) / 2

      chunk_start = max(0, probe - 512)
      chunk_probe = max(512 - probe, 512)
      self._fh.seek(chunk_start)
      chunk = self._fh.read(1024)
      
      prev_nl_offset = chunk.rfind("\n", 0, chunk_probe)
      next_nl_offset = chunk.find("\n", chunk_probe)

      if prev_nl_offset > -1 and prev_nl_offset < next_nl_offset:
        line = chunk[prev_nl_offset+1:next_nl_offset]
      else:
        line = chunk[:next_nl_offset]

      entry = LogEntry(line)
      if entry.time_object < start_time:
        low = probe
      else:
        high = probe

    self._fh.seek(low + 1)

  def __iter__(self):
    return self

  def next(self):
    start_pos = self._fh.tell()
    chunk = self._fh.read(512)
    if not chunk:
      raise StopIteration

    offset = chunk.find("\n")
    ret = chunk[:offset]
    self._fh.seek(offset + 1 + start_pos)
    return ret

def split_line(line):
  fields = [s.strip() for s in line.split(",")]
  ret = list()
  quoting = False
  for field in fields:
    if quoting:
      ret[-1] += field
    else:
      ret.append(field)

    if ret[-1][0] == ret[-1][-1] == '"':
      ret[-1] = ret[-1][1:-1]

    if field[0] == field[-1] == '"':
      continue

    if field[-1] == '"':
      quoting = False
    if field[0] == '"':
      quoting = True

  assert not quoting
  return ret

def fatal(msg):
  print >> sys.stderr, msg
  sys.exit(1)

class Histogram(object):
  def __init__(self, num_buckets):
    self._data = []
    self._num_buckets = num_buckets

  def Add(self, amt):
    self._data.append(amt)

  def Print(self):
    buckets = [0] * self._num_buckets
    self._data.sort()
    max_data = max(self._data)
    min_data = min(self._data)

    bucket_size = int((max_data - min_data) / (self._num_buckets - 1))
    for amt in self._data:
      bucket = int((amt - min_data) / bucket_size)
      buckets[bucket] += 1

    for i in range(self._num_buckets):
      low = bucket_size * i + min_data
      high = bucket_size * (i + 1) + min_data
      print "%5d - %5d: %3d" % (low, high, buckets[i])

class PlayerSpellDamage(object):
  def __init__(self, spell_name, player_name):
    self.spell = spell_name
    self.player = player_name
    self.total_damage = 0
    self.total_resists = 0
    self.total_resisted = 0
    self.partial_resists = 0
    self.num_casts = 0
    self.full_resists = 0

  def AddFullResist(self):
    self.total_resists += 1
    self.num_casts += 1
    self.full_resists += 1

  def AddPartialResist(self, damage, resisted):
    self.num_casts += 1
    if resisted:
      self.total_resists += 1
    self.total_damage += damage
    self.total_resisted += resisted

    if damage > 0 and resisted > 0:
      self.partial_resists += 1
    elif damage == 0 and resisted > 0:
      self.full_resists += 1


INTEGER_FIELDS = set("amount resisted blocked absorbed critical glancing crushing overkill overheal".split())

class LogEntry(object):
  _basic_line_re = re.compile(r'(\d{1,2})/(\d{1,2})  *(\d\d):(\d\d):(\d\d)\.(\d+)  *(.*)$')
  _tmptime = time.localtime()
  _year, _dst = _tmptime[0], _tmptime[-1]

  def __init__(self, line):
    self._idx = 0
    self.raw_line = line
    m = self._basic_line_re.match(line)
    assert m
    self.epoch_time = time.mktime((self._year, int(m.group(1)), int(m.group(2)),
                                   int(m.group(3)), int(m.group(4)), int(m.group(5)),
                                   0, 0, self._dst)) + int(m.group(6))/1000.0
    self.time_object = datetime.datetime(self._year, int(m.group(1)), int(m.group(2)),
                                         int(m.group(3)), int(m.group(4)), int(m.group(5)),
                                         1000 * int(m.group(6)))
    self.raw_line_no_time = m.group(7)
    self._fields = split_line(m.group(7))
    self.type = self._Fields(1)

    self._split_type = self.type.split("_")
    self.prefix = self._split_type[0]
    self.suffix = self._split_type[-1]

    self.source_id, self.source_name  = self._Fields(2)
    self.source_flags = int(self._Fields(1), 16)
    self.dest_id, self.dest_name = self._Fields(2)
    self.dest_flags = int(self._Fields(1), 16)

    if self.prefix in ('SPELL', 'RANGE'):
      self.spell_id, self.spell_name, self.spell_school = self._Fields(3)
    elif self.prefix == 'ENVIRONMENTAL':
      self.environmental_type = self._Fields(1)

    if self.type == 'SPELL_MISSED' or self.type == 'SPELL_PERIODIC_MISSED':
      self.missed_reason = self._Fields(1)
    if self.type == 'SWING_MISSED':
      self.missed_reason = self._Fields(1)

    if self.type == 'SPELL_AURA_APPLIED' or self.type == 'SPELL_AURA_REMOVED':
      self.buff_debuff = self._Fields(1)

    if self.suffix == 'DAMAGE':
      (self.amount, self.overkill, self.school, self.resisted, self.blocked, self.absorbed,
       self.critical, self.glancing, self.crushing) = self._Fields(9)

    if self.suffix == "HEAL":
      (self.amount, self.overheal, self.critical) = self._Fields(3)

    if self.type == 'SPELL_DISPEL':
      (self.dispel_spell_id, self.dispel_spell_name,
       self.dispel_flag, self.dispel_attr) = self._Fields(4)

    for field in INTEGER_FIELDS:
      if hasattr(self, field):
        value = getattr(self, field)
        if value == 'nil': value = "0"
        setattr(self, field, int(value))

  def _Fields(self, n):
    self._idx += n
    if n == 1:
      return self._fields[self._idx - 1]
    else:
      return self._fields[self._idx - n:self._idx]

  def PrettyTimestamp(self):
    p = time.strftime("%m/%d %H:%M:%S", time.localtime(self.epoch_time))
    return "%s.%03d" % (p, int(1000 * self.epoch_time) % 1000)

  def IsEffectiveDamage(self):
    return (self.suffix == 'DAMAGE' or
            (self.suffix == 'MISSED' and self.missed_reason == 'ABSORB'))

  def SourceIsRaidMember(self, pets=True):
    if pets:
      mask_list = (COMBATLOG_OBJECT_TYPE_PLAYER, COMBATLOG_OBJECT_TYPE_PET)
    else:
      mask_list = (COMBATLOG_OBJECT_TYPE_PLAYER,)

    return ((self.source_flags & COMBATLOG_OBJECT_TYPE_MASK) in mask_list and 
            (self.source_flags & COMBATLOG_OBJECT_AFFILIATION_MASK in
             (COMBATLOG_OBJECT_AFFILIATION_RAID,
              COMBATLOG_OBJECT_AFFILIATION_PARTY,
              COMBATLOG_OBJECT_AFFILIATION_MINE)))

  def DestIsRaidMember(self, pets=True):
    if pets:
      mask_list = (COMBATLOG_OBJECT_TYPE_PLAYER, COMBATLOG_OBJECT_TYPE_PET)
    else:
      mask_list = (COMBATLOG_OBJECT_TYPE_PLAYER,)

    return ((self.dest_flags & COMBATLOG_OBJECT_TYPE_MASK) in mask_list and
            (self.dest_flags & COMBATLOG_OBJECT_AFFILIATION_MASK in
             (COMBATLOG_OBJECT_AFFILIATION_RAID,
              COMBATLOG_OBJECT_AFFILIATION_PARTY,
              COMBATLOG_OBJECT_AFFILIATION_MINE)))
  def __str__(self):
    return "<LogEntry: '%s'>" % self.raw_line

def RegisterSinks(module, parser):
  ret = dict()
  for entry_name in dir(module):
    entry = getattr(module, entry_name)
    if (type(entry) == type(clogger_sinks.EventSink) and
        issubclass(entry, clogger_sinks.EventSink) and
        entry != clogger_sinks.EventSink):
      ret[entry.mode_name] = entry
      if hasattr(entry, "RegisterOptions"):
        group = optparse.OptionGroup(parser, entry.mode_desc, entry.mode_desc)
        entry.RegisterOptions(group)
        parser.add_option_group(group)
  return ret

def main(args):
  parser = optparse.OptionParser(usage=__doc__.rstrip())
  parser.add_option("-s", "--source", help="event source")
  parser.add_option("-d", "--dest", help="event destination")
  parser.add_option("-t", "--type", help="event type")
  parser.add_option("-n", "--spell-name", help="name of spell")
  parser.add_option("", "--suffix", help="event suffix to display")
  parser.add_option("", "--prefix", help="event prefix to display")
  parser.add_option("", "--buffs", help="print only buffs", action="store_true")
  parser.add_option("", "--debuffs", help="print only debuffs", action="store_true")

  parser.add_option("", "--start-time", help="time to begin analysis")
  parser.add_option("", "--end-time", help="time to end analysis")

  all_sinks = RegisterSinks(clogger_sinks, parser)

  if len(args) < 2 or args[1] == 'help' or args[1][0] == "--help":
    parser.print_help()
    print
    print "Available modes: %s" % (", ".join(sorted(all_sinks.keys())))
    sys.exit(1)

  options, args = parser.parse_args(args)
  sink_name = args[1]
  event_sink = all_sinks[sink_name]()

  event_sink.options = options
  if options.source:
    event_sink.AddFilter("source_name", options.source)
  if options.dest:
    event_sink.AddFilter("dest_name", options.dest)
  if options.type:
    event_sink.AddFilter("type", options.type)
  if options.suffix:
    event_sink.AddFilter("suffix", options.suffix)
  if options.prefix:
    event_sink.AddFilter("prefix", options.prefix)
  if options.spell_name:
    event_sink.AddFilter("spell_name", options.spell_name)
  if options.buffs:
    event_sink.AddFilter("buff_debuff", "BUFF")
  if options.debuffs:
    event_sink.AddFilter("buff_debuff", "DEBUFF")

  start_time = None
  end_time = None
  if options.start_time:
    start_time = ParseTime(options.start_time)
  if options.end_time:
    end_time = ParseTime(options.end_time)

  reader_iter = LogReader(args[2], start_time)

  num_lines = 0
  for entry in reader_iter:
    line = entry
      
    num_lines += 1
    if event_sink.FastFilteredOut(line):
      continue

    good_event_type = len(event_sink.interesting_event_types) == 0
    for event_type in event_sink.interesting_event_types:
      if line.find(event_type) >= 0:
        good_event_type = True

    if not good_event_type:
      continue

    entry = LogEntry(line.strip())
    if end_time and entry.time_object > end_time:
      break

    if event_sink.SlowFilteredOut(entry):
      continue

    event_sink.Feed(entry)

  event_sink.Finalize()

if __name__ == '__main__':
  main(sys.argv)
